Docly Developers
Knowledge base Misconception By design

Scale and convert images in Docly

In shortDocly scales and converts images on serving, but the URL is not /image.jpg/WxHxM/name.webp — it is /image.jpg/<embeddedId>/WxHxM/name.webp. Every uploaded file is a document, and the embeddedId is the File property on the object returned by docly.getFile(path). Without that segment the request is a plain 404, which reads like scaling is unsupported. A 400 means the path is right but the size is not in images.allowedSizes.
Applies to Imagesimages.jsonsite.jsonHash templates
  1. What you'll see

    A developer — or an AI coding agent — needs a smaller or reformatted copy of an image that already sits in the site's asset folder, and concludes that Docly cannot do it. The usual path to that wrong conclusion:

    • Query parameters are tried first: /img/photo.webp?w=1200, ?width=1200. They return the original, unscaled, at 200 — so they look like they silently do nothing.
    • The documented URL form is tried next: /img/photo.jpg/1200x675x0/photo.webp. It returns 404.

    The conclusion drawn is "no image tooling available, re-export the file by hand". Both observations are real; the conclusion is wrong.

  2. What's actually happening

    Every uploaded file in Docly is a document, not a loose file on disk. A JPEG dropped in an asset folder is stored with schema Uploaded Image, and the actual bytes live as an embedded file inside that document. The public path (/img/photo.jpg) resolves to the document; the bytes are addressed by an embedded id.

    That is why the scaling URL needs four segments, not three:

    /<public path>/<embeddedId>/<WxHxM>/<display name>.<format>

    Leaving out the embeddedId does not produce a helpful error. The router simply finds no route and answers 404 — indistinguishable from "this feature does not exist". That single missing segment is what sends people down the wrong path.

    Reading the status code

    404The URL shape is wrong — almost always the missing embeddedId segment.
    400The shape is right. The size is rejected by images.sizePolicy. Under auto with both allowedSizes and discoveredSizes empty there is nothing to snap to, so a size is refused until a rendered page has used it (rendered pages add their sizes to discoveredSizes).
    200Working. The extension on the display name decided the output format.

    Let linkImage build the URL

    linkImage() builds exactly this URL for you, and it is available in every server-side context: display templates, .hash pages, master pages and API scripts. For an image that is not on the document being rendered, pass the file object from docly.getFile() or docly.getFiles() as the first argument. The URL only has to be assembled by hand where no server-side code runs — a static CSS file, client-side JavaScript, or a reference from outside the site — and then the embeddedId has to be looked up first.

    Scaling up costs more than it saves

    The URL has no quality parameter. The encoder quality comes from quality in #/images.json — one number for every format, or per format such as { "jpeg": 90, "webp": 78, "avif": 50, "default": 70 } — and from a built-in default when it is not set. PNG is lossless and ignores it. Requesting a size larger than needed re-encodes from the full-resolution source and can produce a file heavier than the original. Measured on a 199 kB JPEG whose existing WebP was 155 kB: 1600x900x0 → 246 kB, 1100x620x0 → 151 kB, 800x450x0 → 96 kB. Always verify the byte count of the derivative instead of assuming a smaller box means a smaller file.

  3. What to do

    1. Find the embeddedId with a probe file

    Write a throwaway .hash that dumps the file object. This is the fastest way to see what you are actually working with, and it costs nothing to delete afterwards.

    #setmime("text/plain")#
    #{
        let fi = docly.getFile("#/Root/img/photo.jpg");
        write(JSON.stringify(fi));
    }#

    Note the path form: filesystem functions take a single #/, not the ##/ used by #include() in bundle files. The response contains:

    {
      "File":      "7ba189d5-b124-4eb6-b30c-82a817adb909.jpg",   <-- embeddedId
      "Title":     "photo.jpg",
      "filepath":  "/#/Root/img/photo.jpg",
      "fileschema": "Uploaded Image",
      "Filesize":  199170
    }

    File is the embeddedId. Delete the probe when you are done — it is served publicly like any other page.

    2. Register the sizes you intend to use

    In #/images.json (or the images section of #/site.json). With auto and empty lists a scaled request returns 400 until a rendered page has used that size — declare the sizes you use:

    {
      "sizePolicy": "auto",
      "defaultFormat": "webp",
      "onDisallowed": "nearest",
      "maxDimension": 2000,
      "allowedSizes": ["1600x900x0", "1100x620x0", "800x450x0"],
      "discoveredSizes": []
    }

    WebP and AVIF also require sizePolicy to be auto or strict — under the default open a .webp display name is served as JPEG regardless of the extension.

    3. Assemble the URL

    /img/photo.jpg/7ba189d5-b124-4eb6-b30c-82a817adb909.jpg/800x450x0/photo.webp
    \_________/ \_________________________________________/ \________/ \_______/
     public path              embeddedId                      WxHxM     name.format

    Usable anywhere a URL is: a CSS background-image, a static <img src>, an Open Graph tag. The extension on the last segment picks the format — .jpg, .png, .webp, .avif. .svg and .gif are never converted.

    In template code, do not write this URL out. The breakdown above is here so you can read and debug a scaled URL — not as a construction recipe. From #JS use docly.linkImage(Image1, 800, 450, 0, 'photo.webp'), or docly.linkImage(f, f.Image1, …) when the image lives on another document; docly.linkFile takes the same two forms for non-image files. Assembling the path by hand skips the encoding the functions do and hides the size from the Image sizes → Scan, so it will not be registered in allowedSizes. See Do not build file and image URLs by hand. Hand-assembly is for the places a function cannot run — client-side JavaScript and references from outside the site.

    4. Measure the result

    Request the derivative and compare bytes against the original before committing to it. If the derivative is heavier, pick a smaller box or lower quality in #/images.json — the URL itself has no quality parameter.

    When to reach for scaleImage instead

    docly.scaleImage(bytes, w, h, mode, quality, format) runs server-side, takes and returns bytes, exposes quality, and is not subject to the size policy. Use it when you need a one-off derivative with its own quality, outside the size policy. scaleImage is available in pages, templates and API scripts; writing the result to a file with docly.saveFile() needs an API script, because saveFile exists only there.

    Rule of thumb

    Docly scales and converts images on serving. The URL is filepath / embeddedId / WxHxM / name.format — and the embeddedId is the File property from docly.getFile(). A 404 means you left it out; a 400 means the size is not registered in images.allowedSizes. Never conclude from a 404 that scaling is unavailable.

Last reviewed 17 Oct 2026